<!DOCTYPE html>
<html>

<head>
  <title>用户A-语音通话</title>
  <style>
    body {
      margin: 0;
      padding: 0;
      width: 100%;
      height: 100vh;
      display: flex;
      justify-content: center;
    }

    .box {
      display: flex;
      flex-direction: column;
      align-items: center;
    }

    .box:first-child {
      margin-right: 20px;
    }

    .container {
      width: 370px;
      height: 580px;
      background-color: #f5f5f5;
      margin-bottom: 20px;
      position: relative;
    }

    button {
      width: 100px;
      height: 40px;
      cursor: pointer;
      background-color: #fff;
      border: 1px solid #ccc;
      color: #555;
      font-size: 15px;
    }

    button:first-child {
      margin-right: 20px;
    }

    video {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    .container-small {
      width: 120px;
      height: 160px;
      position: absolute;
      right: 0;
      bottom: 0;
      z-index: 1;
    }

    .layer {
      width: 100%;
      height: 100%;
      position: absolute;
      z-index: 2;
      top: 0;
      left: 0;
      background-color: rgba(0, 0, 0, 0.8);
      color: #fff;
      display: flex;
      flex-direction: column;
      align-items: center;
      display: none;
    }

    .layer__text {
      margin-top: 150px;
      margin-bottom: 260px;
    }

    .layer__btns {
      display: flex;
    }

    .layer__btns .layer__btn:last-child {
      margin-left: 100px;
    }

    .layer__btn {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      font-size: 12px;
      cursor: pointer;
    }

    .layer__btn img {
      border-radius: 50%;
      background-color: #fff;
      margin-bottom: 8px;
    }
  </style>
</head>

<body>
  <div class="box">
    <h1>用户A</h1>
    <div class="container">
      <video id="videoA"></video>
      <div class="container-small">
        <video id="videoASmall"></video>
      </div>
      <div id="layerA" class="layer">
        <div class="layer__text">正在等待对方接受邀请...</div>
        <div class="layer__btns">
          <div class="layer__btn" id="cancelBtn">
            <img
              src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6400a568de2e4a57b04e684be4004d52~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=48&h=48&s=870&e=png&a=1&b=f53c36" />
            <span>取消</span>
          </div>
        </div>
      </div>
      <div id="hangupLayer" class="layer">
        <div class="layer__text">通话已挂断</div>
      </div>
    </div>
    <div>
      <button id="videoBtn">视频通话</button>
      <button id="hangupBtn">挂断</button>
    </div>
  </div>
  <script type="module">
    import { io } from "https://cdn.socket.io/4.7.5/socket.io.esm.min.js";
    document.addEventListener('DOMContentLoaded', () => {
      // 初始化socket
      const socket = io('http://localhost:3000/');
      const roomId = '123456';
      socket.on('connectionSuccess', () => {
        console.log('连接成功');
        socket.emit('joinRoomEvent', roomId);
      });

      const videoBtn = document.getElementById('videoBtn');
      const hangupBtn = document.getElementById('hangupBtn');
      const cancelBtn = document.getElementById('cancelBtn');
      const layerA = document.getElementById('layerA');
      const hangupLayer = document.getElementById('hangupLayer');
      // video
      const videoA = document.getElementById('videoA');
      const videoASmall = document.getElementById('videoASmall');
      // stream
      let streamA = null;
      // 呼叫中
      let calling = false;
      // 通话中
      let communicating = false;

      // 视频通话
      videoBtn.addEventListener('click', async () => {
        hangupLayer.style.display = 'none';
        calling = true;
        layerA.style.display = 'flex';
        const stream = await getLocalStream();
        videoA.srcObject = stream;
        videoA.play();
        streamA = stream;
        // 申请通话
        socket.emit('callEvent', roomId);
      });

      let peer = null;
      // 监听同意接听事件
      socket.on('acceptCallEvent', async () => {
        peer = new RTCPeerConnection();
        // 添加本地"音视频流"
        for (const track of streamA.getTracks()) {
          peer.addTrack(track, streamA);
        }
        peer.onicecandidate = event => {
          if (event.candidate) {
            // 将candidate发送给对方
            socket.emit('candidateEvent', { roomId, candidate: event.candidate });
          }
        }
        peer.ontrack = event => {
          calling = false;
          layerA.style.display = 'none';
          communicating = true;
          videoASmall.srcObject = event.streams[0];
          // 让video缓一下，再播放，否则可能存在流还没添加上，就播放视频而报错
          setTimeout(() => {
            videoASmall.play();
          }, 100);
        }
        // 生成offer
        const offer = await peer.createOffer({
          offerToReceiveAudio: 1,
          offerToReceiveVideo: 1
        });
        // 设置本地描述的offer
        await peer.setLocalDescription(offer);
        // 将offer发送给对方
        socket.emit('offerEvent', { roomId, offer });
      });
      socket.on('answerEvent', answer => {
        // 设置"远端"描述的answer
        peer.setRemoteDescription(answer);
      });
      socket.on('candidateEvent', async candidate => {
        await peer.addIceCandidate(candidate);
      });

      // 挂断通话
      hangupBtn.addEventListener('click', () => {
        if (communicating) {
          hangupCall();
          socket.emit('end', roomId);
        }
      });

      // 取消呼叫
      cancelBtn.addEventListener('click', () => {
        if (calling) {
          hangupCall();
          socket.emit('cancelCallEvent', roomId);
        }
      });
      // 监听挂断事件
      socket.on('end', () => {
        alert("对方已挂断")
        hangupCall();
        hangupLayer.style.display = 'flex';
      });
      function hangupCall() {
        if (peer) {
          peer.close();
          peer = null;
        }
        if (streamA) {
          streamA.getTracks().forEach(track => track.stop());
          streamA = null;
        }
        videoA.srcObject = null;
        videoASmall.srcObject = null;
        communicating = false;
        calling = false;
        layerA.style.display = 'none';
      }

      /** @name 获取本地音视频流 **/
      async function getLocalStream() {
        const stream = await navigator.mediaDevices.getUserMedia({
          audio: true,
          video: true
        });
        return stream;
      }
    });
  </script>
</body>

</html>